package render.graph
{
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.geom.Rectangle;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	
	import mx.containers.Canvas;
	import mx.controls.Label;
	import mx.core.UIComponent;
	import mx.effects.Effect;
	import mx.events.EffectEvent;
	
	import render.NTData;
	
	public class GraphRender extends Canvas
	{
		protected var _graph:Graph;
		protected var _ntData:NTData;
		protected var _edgeRender:EdgeRender;
		protected var _nodeRender:NodeRender;
		
		protected var maxNeighborSize:int = 10;
		
		private var _forcefitAdjuster:ExternalAdjuster;
		
		private var _addDisplayEffect:Effect;
		private var _removeDisplayEffect:Effect;
		private var _repulsionFactor:Number = 0.65;
		private var _showEdgeName:Boolean = true;
		private var _showAllNodes:Boolean = false;
		private var _staticGraph:Boolean = true;
		
		// args: uri,startIndex,pageSize
		// return an array of triples
		private var _fetcherFunction:Function;
		
		private var backgroundSurface:Canvas;
		private var timer:Timer;
		private var preConverage:Number = 0;
		private var dragingDisplay:UIComponent = null;
		private var dragStartX:int = 0;
		private var dragStartY:int = 0;
		private var lastCursorX:int = 0;
		private var lastCursorY:int = 0;
		
		private var currentRootURI:String;
		
		
		private static const DELTA:Number = 0.03;
		
		public function GraphRender()
		{
			backgroundSurface = new Canvas();
			this.addChild(backgroundSurface);
			this.verticalScrollPolicy = "off";
			this.horizontalScrollPolicy = "off";
			
			_edgeRender = new DefaultEdgeRender();
			_nodeRender = new DefaultNodeRender();
			
			_forcefitAdjuster = new AlphaAdjuster(this);
			
			this.addEventListener(MouseEvent.MOUSE_MOVE,dragingUICompoment);
			this.addEventListener(MouseEvent.MOUSE_UP,dragUICompomentEnd);
		}
		public function set showAllnodes(show:Boolean):void
		{
			_showAllNodes = show;	
			if( _ntData)
				reInit();
		}
		protected function reInit():void
		{
			this.removeAllChildren();
			
			backgroundSurface = new Canvas();
			this.addChild(backgroundSurface);
			
			_graph = new Graph(_ntData,_showAllNodes);
			
			for each(var node:Node in _graph.nodeArray)
				drawNode(node);

			for each(var node:Node in _graph.allNodes)
				createNode(node);
			refresh();			
		}
		private function createNode(node:Node):void
		{
			if(node.display != null)
				return;
				
			var display:UIComponent = renderNode(node);
			
			trace(" x " + display.x );
			trace(" y " + display.y );
			
			display.x = this.width / 2 ;
			display.y = this.height / 2;
			display.visible = false;
			
			node.display = display;
			
			backgroundSurface.addChild(display);
			display.addEventListener(MouseEvent.MOUSE_DOWN,dragDisplay);	
		}
		override protected function updateDisplayList(unscaledWidth:Number,unscaledHeight:Number):void
		{
			super.updateDisplayList(unscaledWidth,unscaledHeight);
			
			if((_graph != null ) && _graph.layoutChanged){
				draw();
				_graph.layoutChanged = false;
			}
		}
		private function draw():void{
			backgroundSurface.graphics.clear();
			
			for each(var edge:Edge in _graph.edgeArray){
				drawEdge(edge);
			}
		}
		private function drawEdge(edge:Edge):void{
			var fromNode:Node = edge.fromNode;
			var toNode:Node = edge.toNode;
			
			var displayF:UIComponent = fromNode.display;
			var displayT:UIComponent = toNode.display;
			
			var fx:int = displayF.x + (displayF.width/2) ;
			var fy:int = displayF.y + (displayF.height/2);
			var tx:int = displayT.x + (displayT.width/2); 
			var ty:int = displayT.y + (displayT.height/2);
			
			_edgeRender.renderEdge(backgroundSurface,fx,fy,tx,ty,edge);		
			
			if(_showEdgeName)
				_edgeRender.renderEdgeName(backgroundSurface,(fx+tx)/2,(fy+ty)/2,edge);
		}
		public function set showEdgeName(show:Boolean):void
		{
			_showEdgeName = show;
			reInit();
		}
		public function drawNode(node:Node):void
		{
			var display:UIComponent = renderNode(node);
			display.x = this.width / 2;
			display.y = this.height / 2;
			
			node.display = display;
			
			addDisplay(display);
		}
		public function eraseNode(node:Node):void
		{
			var display:UIComponent = node.display;
			
			if(_removeDisplayEffect != null){
				_removeDisplayEffect.addEventListener(EffectEvent.EFFECT_END,eraseDone);
				_removeDisplayEffect.createInstance(node).startEffect();
			}
			else
				backgroundSurface.removeChild(node.display);
			
		}
		private function eraseDone(event:EffectEvent):void
		{
			var ui:UIComponent = event.effectInstance as UIComponent;
			if(ui.parent != null)
				ui.parent.removeChild(ui);
		}
		private function renderNode(node:Node):UIComponent
		{
			return _nodeRender.renderNode(node);
		}
		private function addDisplay(ui:UIComponent):void
		{
			backgroundSurface.addChild(ui);

			ui.addEventListener(MouseEvent.MOUSE_DOWN,dragDisplay);			

			if(_addDisplayEffect != null)
				_addDisplayEffect.createInstance(ui).startEffect();
		}
		private function dragUICompomentEnd(event:MouseEvent):void
		{
			_forcefitAdjuster.removeFixedDisplay(dragingDisplay);
			dragingDisplay = null;
		}
		private function dragingUICompoment(event:MouseEvent):void
		{
			if(dragingDisplay == null||dragingDisplay == backgroundSurface) return;
			
			var dx:int = event.stageX - lastCursorX;
			var dy:int = event.stageY - lastCursorY;
			
			dragingDisplay.x = dragStartX + dx;
			dragingDisplay.y = dragStartY + dy;
			
			refresh();
		}
		private var lastClickTime:int = 0;
		private var lastExpanded:Node = null;
		private function dragDisplay(event:MouseEvent):void
		{
			dragingDisplay = event.currentTarget as UIComponent;
			
			var now:int = getTimer();
			if( (now - lastClickTime) < 300){
				
				if(_staticGraph)
					return;
					
				var clickNode:Node = null;
				for each(var node:Node in _graph.nodeArray)
					if(dragingDisplay == node.display){
						clickNode = node;
						break;
					}
				
				if(clickNode == lastExpanded)
					return;
				
				if(clickNode.type == Node.ATTR)
					return;
					
				if(_fetcherFunction != null)
					fetch(clickNode);
				
				if(lastExpanded != null)
					_graph.hideAttr(lastExpanded);
				_graph.showAttr(clickNode);
							
				lastExpanded = clickNode;
				
				refresh();
				return;
			}
			lastClickTime = now;
			
			dragStartX = dragingDisplay.x;
			dragStartY = dragingDisplay.y;
			lastCursorX = event.stageX;
			lastCursorY = event.stageY;
			
			_forcefitAdjuster.addFixedDisplay(dragingDisplay);
			 			
			event.stopImmediatePropagation();
		}
		private function fetch(node:Node):void
		{
			//trace(' try to fetch ');
			trace(" fetch neighbors of Node: " + node.text);
				
			reInitRoot(node.text);
		}
		protected function reInitRoot(uri:String):void
		{
			currentRootURI = uri;
			_fetcherFunction.call(this,fetchHandler,uri,0,maxNeighborSize+1);
		}
		private function fetchHandler(array:Array):void
		{
			var tooManyNeighbors:Boolean = false;
			if(array.length > maxNeighborSize){
				tooManyNeighbors = true;
				array = array.slice(0,maxNeighborSize-1);
			}
				
			_ntData = new NTData(null,array);
			reInit();	
			
			if(tooManyNeighbors)
				info("Clicked node has too many neighbors, choose tree/list mode for complete view.");
			
			_graph.addMustShow(_graph.getNode(currentRootURI));			
		}
		private function info(text:Object):void
		{
			trace(text);
			var l:Label = new Label()
			l.text = text.toString();
			l.x = 0;
			l.y = 0;
			l.height = 16;
			l.width = Math.min(width,l.textWidth);
			l.setStyle("fontSize","11");
			l.setStyle("fontStyle","italic");
			l.visible = true;
			backgroundSurface.addChild(l);
		}
		private function clk(event:TimerEvent = null):void
		{
			autofitAdjust();	
			this.invalidateDisplayList();
			resetTimer();
		}
		private function resetTimer():void
		{
			timer = new Timer(10,2);
			timer.addEventListener(TimerEvent.TIMER_COMPLETE,clk);
			timer.start();
		}
		/**
		 * This cover adjust algorithm is derived from Macromedia's Spring Graph 
		 */
		private function autofitAdjust():void
		{
			_forcefitAdjuster.adjust();
			
			var bound:Rectangle = getBound();
			var repulsionFactor:Number = _repulsionFactor;
			
			if(this.getChildren().length != 0){
				var verticalConverage:Number = ( bound.bottom - bound.top ) / this.height;
				var horizontalConverage:Number = ( bound.right - bound.left ) / this.width;
				
				var converage:Number = Math.max(verticalConverage,horizontalConverage);
				
				if((preConverage > 0) && (converage > 0)){
					var distance:Number = 0.9 - converage;
					if(Math.abs(distance) > DELTA){
						var deltaConverage:Number = converage - preConverage;
						
						var targetDelta:Number = distance * 0.2;
						targetDelta = Math.max(-0.01,targetDelta);
						targetDelta = Math.min(0.01,targetDelta);
						
						if(deltaConverage < targetDelta){
							repulsionFactor = Math.min(_repulsionFactor + 0.01,0.7);
						}
						else{
							repulsionFactor = Math.max(_repulsionFactor - 0.01,0.05);
						}
					}
				}
				
				preConverage = converage;
				
				if((bound.left < 0) || (bound.top < 0) || (bound.bottom > this.height) || (bound.right > this.width)) {
					
					var scrollX: int = (this.width / 2) - (bound.x + (bound.width / 2));
					if(scrollX < -1) scrollX = -2;
					if(scrollX > 1) scrollX = 2;
					
					var scrollY: int = (this.height / 2) - (bound.y + (bound.height / 2));
					if(scrollY < -1) scrollY = -2;
					if(scrollY > 1) scrollY = 2;
					
					if((scrollX != 0) || (scrollY != 0))
						scroll(scrollX, scrollY);
				}
				
				
				var sumX:int = 0;
				var sumY:int = 0;
				for each (var child:UIComponent in backgroundSurface.getChildren()){
					sumX += child.x;
					sumY += child.y;
				}
				var avgX:int = sumX / backgroundSurface.getChildren().length;
				var avgY:int = sumY / backgroundSurface.getChildren().length;
				
				var lowX:int = width / 5;
				var lowY:int = height / 5;
				var highX:int = width * ( 4 / 5.0 );
				var highY:int = height * ( 4 / 5.0 );
				
				var step:int = 2;
				
				if( avgX < lowX || avgX > highX ){
					for each( var child:UIComponent in backgroundSurface.getChildren()){
						if( child.x< lowX )
							child.x += step;
						if( child.x> highX)
							child.x -= step;
					}
				}
				if( avgY < lowX || avgY > highY ){
					for each( var child:UIComponent in backgroundSurface.getChildren()){
						if( child.y< lowY )
							child.y += step;
						if( child.y> highY)
							child.y -= step;
					}
				}				
				
			}
			
			_repulsionFactor = repulsionFactor;
			
		}
		public function set staticGraph(bool:Boolean):void
		{
			_staticGraph = bool;
		}
		public function get staticGraph():Boolean
		{
			return _staticGraph;
		}
		public function set fetcherFunction(func:Function):void
		{
			_fetcherFunction = func;
		}
		public function get repulsionFactor():Number{
			return _repulsionFactor;
		}
		private function refresh():void
		{
			if(_graph != null ){
				_graph.layoutChanged = true;
				if( (_forcefitAdjuster != null) && _graph.nodeArray.length > 0 ){
					_forcefitAdjuster.resetDamper();
					if(timer == null)
						clk();
				}
			}	
		}
		private function getBound():Rectangle
		{
			var r:Rectangle = new Rectangle(9999999, 9999999, -9999999, -9999999);
			for each( var child:UIComponent in this.getChildren() ){
				// skip the background
				if(child == backgroundSurface)
					continue;
					
				r.left = Math.min( child.x , r.left );
				r.right = Math.max( r.right, child.x + child.width);
				r.top = Math.min(r.top,child.y);
				r.bottom = Math.max(r.bottom,child.y + child.height);
			}
			return r;
		}
		public function get graph():Graph
		{
			return _graph;
		}
		private function scroll(deltaX: int, deltaY: int): void {
   			var c: Array = this.getChildren();
   			for (var i: int = 1; i < c.length; i++) {
   				var itemView: Object = c[i];
   				if(itemView != backgroundSurface) {
   					itemView.x = itemView.x + deltaX;
   					itemView.y = itemView.y + deltaY;
   				}
   			}
  		}
	}
}

import mx.core.UIComponent;
import mx.controls.Label;
import flash.display.Graphics;
import render.graph.EdgeRender;
import render.graph.NodeRender;
import render.graph.Edge;
import render.graph.Node;

internal class DefaultEdgeRender implements EdgeRender{
	public function DefaultEdgeRender(){
		
	}
	
	private static const arrowLength:Number = 5;
	private static const arrowWidth:Number = 5 / 2 ;
	
	private static const RELColor:int = 0x99FF00;
	private static const ATTRColor:int = 0x9999FF;
	private static const COLOR_DELTA:int = 0x300000;
	private static const RELFontColor:int = RELColor - COLOR_DELTA * 2;
	private static const ATTRFontColor:int = ATTRColor - COLOR_DELTA;
	private static const DefaultWidth:int = 70;
	private static const DefaultHeight:int = 20;
	
	public function renderEdgeName(backgroundSurface:UIComponent,x:int,y:int,edge:Edge):void
	{
		var label:Label = edge.display as Label;
		if(label == null ) label = createLabel(backgroundSurface,edge);
		label.visible = true;
		label.x = x - (label.width/2);
		label.y = y - (label.height/2);	
	}
	public static function trim(uri:String):String
	{
		if(uri.lastIndexOf(">") != -1 && uri.indexOf("#") != -1 ){
			return uri.substring(uri.indexOf("#")+1,uri.lastIndexOf(">"));
		}
		if(uri.lastIndexOf('"') != uri.indexOf('"'))
			return uri.substring(uri.indexOf('"')+1,uri.lastIndexOf('"'));
		return uri;
	}
	private function createLabel(backgroundSurface:UIComponent,edge:Edge):Label
	{
		var l:Label = new Label();
		l.text = trim(edge.text);
		l.height = DefaultHeight;
		l.width = DefaultWidth;
//		l.width = Math.min(DefaultWidth,l.textWidth);
		if(edge.isRel)
			l.setStyle("color",RELFontColor);
		else
			l.setStyle("color",ATTRFontColor);
		
		l.setStyle("fontSize",11);
		backgroundSurface.addChild(l);
		edge.display = l;
		return l;
	}
	public function renderEdge(backgroundSurface:UIComponent,fx:int,fy:int,tx:int,ty:int,edge:Edge):void
	{
		var g:Graphics = backgroundSurface.graphics;
		
		var dx:Number = tx - fx;
		var dy:Number = ty - fy;
		
		tx = fx + dx * 0.9;
		ty = fy + dy * 0.9;
		fx = tx - dx * 0.8;
		fy = ty - dy * 0.8;
		
		if(edge.isRel)
			drawLineWithArrow(g,fx,fy,tx,ty,RELColor);
		else
			drawLine(g,fx,fy,tx,ty,ATTRColor);
	}
	public function drawLine(graphics:Graphics,fx:int,fy:int,tx:int,ty:int,lineColor:int,alpha:Number=1):void
	{
		graphics.moveTo(fx,fy);
		graphics.lineStyle(2,lineColor,alpha);
		graphics.beginFill(0);
		graphics.lineTo(tx,ty);
		graphics.endFill();
	}
	public function drawLineWithArrow(graphics:Graphics,fx:int,fy:int,tx:int,ty:int,lineColor:int,alpha:Number=1):void
	{
		drawLine(graphics,fx,fy,tx,ty,lineColor,alpha);
		drawArrow(graphics,fx,fy,tx,ty,lineColor,alpha);
	}
	public function drawArrow(graphics:Graphics,fx:int,fy:int,tx:int,ty:int,arrowColor:int,alpha:Number=1):void
	{
		var dx:Number = tx - fx;
		var dy:Number = ty - fy;
		var len:Number = Math.sqrt(dx * dx + dy * dy);
		
		var sinA:Number = dy / len;
		var cosA:Number = dx / len;
		
		var midX:Number = tx - cosA * arrowLength;
		var midY:Number = ty - sinA * arrowLength;
		
		var ax:Number = midX + sinA * arrowWidth;
		var ay:Number = midY - cosA * arrowWidth;
		
		var bx:Number = midX - sinA * arrowWidth;
		var by:Number = midY + cosA * arrowWidth;
		
		graphics.lineStyle(2,arrowColor,alpha);
        graphics.beginFill(arrowColor);
        graphics.moveTo(tx, ty);
        graphics.lineTo(ax, ay);
        graphics.lineTo(bx, by);
        graphics.lineTo(tx, ty);
        graphics.endFill();
	}
	
}
internal class DefaultNodeRender implements NodeRender{
	private static const DefaultWidth:int = 100;
	private static const DefaultHeight:int = 20;
	public function DefaultNodeRender(){
		
	}
	public function renderNode(node:Node):UIComponent
	{
		var l:Label = new Label();
		l.height = DefaultHeight;
		l.text = DefaultEdgeRender.trim(node.text);
		l.width = DefaultWidth;
		l.setStyle("textAlign","center");
		l.setStyle("fontSize",12);
		l.setStyle("fontWeight","bold");
		return l;
	}
}
	